/*
 * Copyright (C) 2022
 *
 * Brick Yang <printfxxx@163.com>
 *
 * This program is free software. You can redistribute it and/or
 * modify it as you like.
 */

/**
 * @file	mdio_tool.c
 * @brief	MDIO tool module for linux kernel
 */

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/version.h>
#include <linux/phy.h>
#include <linux/uaccess.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/kallsyms.h>

static int mdio_tool_addr_curr;
static u32 mdio_tool_regnum_curr;
static struct mii_bus *mdio_tool_bus_curr;
static struct proc_dir_entry *pde_mdio_tool, *pde_mdio_tool_bus, *pde_mdio_tool_addr,
			     *pde_mdio_tool_regnum, *pde_mdio_tool_regval;

#if LINUX_VERSION_CODE < KERNEL_VERSION(5, 7, 0)
__weak __alias(_mdio_find_bus)
struct mii_bus *mdio_find_bus(const char *mdio_name);

static struct class *ksym_mdio_bus_class;

static int match_name(struct device *dev, const void *name)
{
	return sysfs_streq(dev_name(dev), name);
}

static struct mii_bus *_mdio_find_bus(const char *mdio_name)
{
	struct device *d;

	d = class_find_device(ksym_mdio_bus_class, NULL, mdio_name, match_name);
	return d ? to_mii_bus(d) : NULL;
}

static int __init compat_init(void)
{
	int rc;

	if (!(ksym_mdio_bus_class = (void *)kallsyms_lookup_name("mdio_bus_class"))) {
		pr_err("Failed to get address of \"mdio_bus_class\"\n");
		rc = -EFAULT;
		goto err;
	}

	return 0;
err:
	return rc;
}
#else
#define compat_init()	(0)
#endif

static int mdio_tool_bus_show(struct seq_file *s, void *v)
{
	if (mdio_tool_bus_curr) {
		seq_printf(s, "%s (%s)\n", mdio_tool_bus_curr->name, dev_name(&mdio_tool_bus_curr->dev));
	} else {
		seq_printf(s, "Not configured\n");
	}

	return 0;
}

static int mdio_tool_bus_open(struct inode *inode, struct file *file)
{
	return single_open(file, mdio_tool_bus_show, NULL);
}

static ssize_t mdio_tool_bus_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
{
	int rc;
	char str[MII_BUS_ID_SIZE];
	static struct mii_bus *new;

	if (copy_from_user(str, buf, sizeof(str))) {
		rc = -EFAULT;
		goto err;
	}
	if (count < sizeof(str)) {
		str[count] = '\0';
	} else {
		str[sizeof(str) - 1] = '\0';
	}

	if (!(new = mdio_find_bus(str))) {
		rc = -ENODEV;
		goto err;
	}

	if (mdio_tool_bus_curr) {
		put_device(&mdio_tool_bus_curr->dev);
	}
	mdio_tool_bus_curr = new;

	return count;
err:
	return rc;
}

static int mdio_tool_addr_show(struct seq_file *s, void *v)
{
	seq_printf(s, "%d\n", mdio_tool_addr_curr);

	return 0;
}

static int mdio_tool_addr_open(struct inode *inode, struct file *file)
{
	return single_open(file, mdio_tool_addr_show, NULL);
}

static ssize_t mdio_tool_addr_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
{
	char str[64];

	if (copy_from_user(str, buf, sizeof(str))) {
		goto err;
	}
	if (count < sizeof(str)) {
		str[count] = '\0';
	} else {
		str[sizeof(str) - 1] = '\0';
	}
	mdio_tool_addr_curr = simple_strtol(str, NULL, 0);

	return count;
err:
	return -EFAULT;
}

static int mdio_tool_regnum_show(struct seq_file *s, void *v)
{
	seq_printf(s, "0x%08x\n", mdio_tool_regnum_curr);

	return 0;
}

static int mdio_tool_regnum_open(struct inode *inode, struct file *file)
{
	return single_open(file, mdio_tool_regnum_show, NULL);
}

static ssize_t mdio_tool_regnum_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
{
	char str[64];

	if (copy_from_user(str, buf, sizeof(str))) {
		goto err;
	}
	if (count < sizeof(str)) {
		str[count] = '\0';
	} else {
		str[sizeof(str) - 1] = '\0';
	}
	mdio_tool_regnum_curr = simple_strtoul(str, NULL, 0);

	return count;
err:
	return -EFAULT;
}

static int mdio_tool_regval_show(struct seq_file *s, void *v)
{
	int rc;
	u16 regval;

	if (!mdio_tool_bus_curr) {
		rc = -ENODEV;
		goto err;
	}

	if ((rc = mdiobus_read(mdio_tool_bus_curr, mdio_tool_addr_curr, mdio_tool_regnum_curr)) < 0) {
		goto err;
	}

	regval = rc & 0xffff;
	seq_printf(s, "0x%04x\n", regval);

	return 0;
err:
	return rc;
}

static int mdio_tool_regval_open(struct inode *inode, struct file *file)
{
	return single_open(file, mdio_tool_regval_show, NULL);
}

static ssize_t mdio_tool_regval_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
{
	int rc;
	u16 regval;
	char str[64];

	if (copy_from_user(str, buf, sizeof(str))) {
		rc = -EFAULT;
		goto err;
	}
	if (count < sizeof(str)) {
		str[count] = '\0';
	} else {
		str[sizeof(str) - 1] = '\0';
	}
	regval = simple_strtoul(str, NULL, 0) & 0xffff;

	if ((rc = mdiobus_write(mdio_tool_bus_curr, mdio_tool_addr_curr, mdio_tool_regnum_curr, regval)) < 0) {
		goto err;
	}

	return count;
err:
	return rc;
}

#if LINUX_VERSION_CODE < KERNEL_VERSION(5, 6, 0)
static const struct file_operations mdio_tool_bus_fops = {
	.owner	 = THIS_MODULE,
	.open	 = mdio_tool_bus_open,
	.read	 = seq_read,
	.write	 = mdio_tool_bus_write,
	.llseek	 = seq_lseek,
	.release = single_release,
};

static const struct file_operations mdio_tool_addr_fops = {
	.owner	 = THIS_MODULE,
	.open	 = mdio_tool_addr_open,
	.read	 = seq_read,
	.write	 = mdio_tool_addr_write,
	.llseek	 = seq_lseek,
	.release = single_release,
};

static const struct file_operations mdio_tool_regnum_fops = {
	.owner	 = THIS_MODULE,
	.open	 = mdio_tool_regnum_open,
	.read	 = seq_read,
	.write	 = mdio_tool_regnum_write,
	.llseek	 = seq_lseek,
	.release = single_release,
};

static const struct file_operations mdio_tool_regval_fops = {
	.owner	 = THIS_MODULE,
	.open	 = mdio_tool_regval_open,
	.read	 = seq_read,
	.write	 = mdio_tool_regval_write,
	.llseek	 = seq_lseek,
	.release = single_release,
};
#else
static const struct proc_ops mdio_tool_bus_fops = {
	.proc_open    = mdio_tool_bus_open,
	.proc_read    = seq_read,
	.proc_write   = mdio_tool_bus_write,
	.proc_lseek   = seq_lseek,
	.proc_release = single_release,
};

static const struct proc_ops mdio_tool_addr_fops = {
	.proc_open    = mdio_tool_addr_open,
	.proc_read    = seq_read,
	.proc_write   = mdio_tool_addr_write,
	.proc_lseek   = seq_lseek,
	.proc_release = single_release,
};

static const struct proc_ops mdio_tool_regnum_fops = {
	.proc_open    = mdio_tool_regnum_open,
	.proc_read    = seq_read,
	.proc_write   = mdio_tool_regnum_write,
	.proc_lseek   = seq_lseek,
	.proc_release = single_release,
};

static const struct proc_ops mdio_tool_regval_fops = {
	.proc_open    = mdio_tool_regval_open,
	.proc_read    = seq_read,
	.proc_write   = mdio_tool_regval_write,
	.proc_lseek   = seq_lseek,
	.proc_release = single_release,
};
#endif

static int __init mdio_tool_init(void)
{
	int rc;

	if ((rc = compat_init())) {
		goto err;
	}

	if (!(pde_mdio_tool = proc_mkdir("mdio_tool", NULL))) {
		rc = -ENOMEM;
		pr_err("Failed to create dir /proc/mdio_tool\n");
		goto err;
	}

	if (!(pde_mdio_tool_bus = proc_create("bus", 0, pde_mdio_tool, &mdio_tool_bus_fops))) {
		rc = -ENOMEM;
		pr_err("Failed to create file /proc/mdio_tool/bus\n");
		goto err_pde_bus;
	}

	if (!(pde_mdio_tool_addr = proc_create("addr", 0, pde_mdio_tool, &mdio_tool_addr_fops))) {
		rc = -ENOMEM;
		pr_err("Failed to create file /proc/mdio_tool/addr\n");
		goto err_pde_addr;
	}

	if (!(pde_mdio_tool_regnum = proc_create("regnum", 0, pde_mdio_tool, &mdio_tool_regnum_fops))) {
		rc = -ENOMEM;
		pr_err("Failed to create file /proc/mdio_tool/regnum\n");
		goto err_pde_regnum;
	}

	if (!(pde_mdio_tool_regval = proc_create("regval", 0, pde_mdio_tool, &mdio_tool_regval_fops))) {
		rc = -ENOMEM;
		pr_err("Failed to create file /proc/mdio_tool/regval\n");
		goto err_pde_regval;
	}

	return 0;

err_pde_regval:
	proc_remove(pde_mdio_tool_regnum);
err_pde_regnum:
	proc_remove(pde_mdio_tool_addr);
err_pde_addr:
	proc_remove(pde_mdio_tool_bus);
err_pde_bus:
	proc_remove(pde_mdio_tool);
err:
	return rc;
}

static void __exit mdio_tool_exit(void)
{
	proc_remove(pde_mdio_tool_regval);
	proc_remove(pde_mdio_tool_regnum);
	proc_remove(pde_mdio_tool_addr);
	proc_remove(pde_mdio_tool_bus);
	proc_remove(pde_mdio_tool);

	if (mdio_tool_bus_curr) {
		put_device(&mdio_tool_bus_curr->dev);
	}
}

module_init(mdio_tool_init);
module_exit(mdio_tool_exit);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("MDIO tool module");
